package com.zmn.mcc.permit.mysql;

import com.zmn.mcc.common.dto.staff.LoginStaffDO;
import com.zmn.mcc.permit.mysql.annotation.ZmnDPermitConfig;
import com.zmn.mcc.permit.mysql.annotation.model.ZmnDPermitConfigModel;
import com.zmn.mcc.permit.mysql.parsing.ZmnDPermitSqlParsing;
import com.zmn.mcc.permit.mysql.targets.ZmnPermitDataTargetUtil;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * 啄木鸟数据权限配置
 * <p>
 * 头部只是标准的Mybatis拦截器写法，注解中的Signature决定了你的代码对哪些方法拦截
 * update实际上针对修改（Update）、删除（Delete）生效，query是对查询（Select）生效。
 *
 * @author duanzuocai
 * @version v1.0
 * @since 2020/6/17 18:55
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ZmnDPermitInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(ZmnDPermitInterceptor.class);

    private static final String TAG = "[zmn]";

    ZmnDPermitSqlParsing zmnPermitDataSqlParsing;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = realTarget(invocation.getTarget());
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);

        if (!isSelect(metaStatementHandler)) {
            return invocation.proceed();
        }

        // DAO方法ID
        String sqlId = metaStatementHandler.getValue("delegate.mappedStatement.id").toString();

        ZmnDPermitConfigModel daoAnnotationConfigModel = getDaoAnnotation(sqlId);

        if (daoAnnotationConfigModel == null) {
            return invocation.proceed();
        }

        // 获取原始sql语句：参数
        Object daoParameterObj = metaStatementHandler.getValue("delegate.boundSql.parameterObject");

        // 传参为空，不做权限限制
        if (daoParameterObj == null) {
            return invocation.proceed();
        }


        String annotationStaffId = daoAnnotationConfigModel.getAnnotation().staffId();
        String annotationLoginStaff = daoAnnotationConfigModel.getAnnotation().loginStaff();

        // 如果不指定权限用户，则数据权限失效（赞不支持获取登录用户）
        if ((annotationStaffId == null || "".equals(annotationStaffId))
                && (annotationLoginStaff == null || "".equals(annotationLoginStaff))) {
            logger.debug("{} SQL拦截，注解没指定权限用户，数据权限失效", TAG);
            return invocation.proceed();
        }

        Object staffObj;

        try {
            staffObj = getStaffObject(daoParameterObj, annotationStaffId, annotationLoginStaff);
        } catch (Exception e) {
            logger.error("{} SQL拦截，入参解析或者MyBatis生成boundSql异常", TAG, e);
            return invocation.proceed();
        }

        if (!checkStaffObj(staffObj)) {
            logger.debug("{} SQL拦截，参数没传入权限用户，数据权限失效。sqlId: {}", TAG, sqlId);
            return invocation.proceed();
        }

        // 原始SQL
        String oldSql = String.valueOf(metaStatementHandler.getValue("delegate.boundSql.sql"));

        Object result;

        logger.info("{} SQL拦截 Interceptor, sqlId: {}, oldSql: {}", TAG, sqlId, oldSql);

        try {
            // 修改SQL，添加Where条件
            // 调用链判断注解
            String newSql = zmnPermitDataSqlParsing.getNewSql(oldSql, daoAnnotationConfigModel, staffObj);

            if (newSql == null) {
                return invocation.proceed();
            }

            logger.info("{} SQL拦截 Interceptor, sqlId: {}, newSql: {}", TAG, sqlId, newSql);

            metaStatementHandler.setValue("delegate.boundSql.sql", newSql);

            // 继续执行
            result = invocation.proceed();
        } catch (Exception e) {
            logger.error("{} MyBatis SQL 拦截器异常, sqlId: {}, 执行原始SQL：{}", TAG, sqlId, oldSql, e);
            return invocation.proceed();
        }

        return result;
    }

    /**
     * 检查登录员工对象
     *
     * @param staffObj 参数类型：Integer, String, LoginStaffDO
     * @return
     */
    private boolean checkStaffObj(Object staffObj) {
        if (staffObj == null) {
            return false;
        }

        if (staffObj instanceof Integer || staffObj instanceof String) {
            try {
                int staffId = Integer.parseInt(staffObj.toString());
                if (staffId == 0) {
                    return false;
                }
                return true;
            } catch (NumberFormatException e) {}
        }

        if (staffObj instanceof LoginStaffDO) {
            try {
                LoginStaffDO staff = (LoginStaffDO) staffObj;
                if (staff.getStaffId() == null || staff.getStaffId() == 0) {
                    return false;
                }
                return true;
            } catch (NumberFormatException e) {}
        }

        return false;
    }

    /**
     * 获取Dao的注解
     *
     * @param sqlId
     * @return 返回null表示没有注解，或者注解因其它原因不生效
     * @throws ClassNotFoundException
     */
    private ZmnDPermitConfigModel getDaoAnnotation(String sqlId) throws ClassNotFoundException {
        ZmnDPermitConfigModel zmnDPermitConfigModel = null;
        ZmnDPermitConfig daoAnnotation = null;

        Class<?> classType = Class.forName(sqlId.substring(0, sqlId.lastIndexOf(".")));

        String mName = sqlId.substring(sqlId.lastIndexOf(".") + 1, sqlId.length());

        // 用于记录重名方法数量
        int mNameNum = 0;

        // 是否需要一个开关，DAO上定义可以提供的方法，暂时不实现
        Method daoMethod = null;
        Method[] daoMethods = classType.getMethods();

        for (Method m : daoMethods) {
            if (!m.getName().equals(mName)) {
                continue;
            }

            mNameNum++;

            daoAnnotation = m.getAnnotation(ZmnDPermitConfig.class);
            zmnDPermitConfigModel = new ZmnDPermitConfigModel(classType.getName(), m.getName(), daoAnnotation);
            if (daoAnnotation == null) {
                continue;
            }

            daoMethod = m;
        }

        // 如果有多个重名方法，则失效，不进行数据权限筛选
        // 如果没有开启注解，不进行数据权限筛选
        if (mNameNum != 1 || daoMethod == null) {
            return null;
        }

        return zmnDPermitConfigModel;
    }

    /**
     * 获取Dao方法访问参数：MAP
     * 例如：areaDao.getByNameOrStaffId(String name, Integer userId)
     *
     * @param daoParameterObj
     * @return Map<name:dzc, userId:10098>
     */
    private Map<Object, Object> getDaoParameter(Object daoParameterObj) {
        Map parameterMap = (MapperMethod.ParamMap) daoParameterObj;

        Map<Object, Object> parameter_new = new HashMap<>(parameterMap.size());

        Iterator parameterKeys = parameterMap.keySet().iterator();
        while (parameterKeys.hasNext()) {
            Object key = parameterKeys.next();
            Object value = parameterMap.get(key);
            parameter_new.put(key, value);
        }

        return parameter_new;
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o,this);
    }

    @Override
    public void setProperties(Properties properties) {
        logger.debug("{} 初始化setProperties配置。。。。", TAG);

        String dialect = properties.getProperty("dialect");
        logger.info("mybatis intercept dialect:{}", dialect);

    }

    /**
     * 获取用户传入的ID或者登录对象“LoginStaffDO"
     *
     * @param daoParameterObj 方法入参
     * @param annotationStaffId 注解申明的参数key
     * @param annotationLoginStaff 注解申明的参数key
     * @return
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private Object getStaffObject(Object daoParameterObj, String annotationStaffId, String annotationLoginStaff)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        // 数据权限员工对象，从Dao的入参中获取
        Object staffObj;

        // MyBatis的DAO上的入参如果只有一个，则对象类型为Object, 如果是多个则为Map
        if (daoParameterObj instanceof Map) {
            Map daoParameterMap = getDaoParameter(daoParameterObj);

            staffObj = ZmnPermitDataTargetUtil.getFieldByMap(annotationLoginStaff, daoParameterMap);

            if (staffObj == null) {
                staffObj = ZmnPermitDataTargetUtil.getFieldByMap(annotationStaffId, daoParameterMap);
            }
        } else {
            staffObj = ZmnPermitDataTargetUtil.getFieldByObj(annotationLoginStaff, daoParameterObj);
            if (staffObj == null) {
                staffObj = ZmnPermitDataTargetUtil.getFieldByObj(annotationStaffId, daoParameterObj);
            }
        }
        return staffObj;
    }

    /**
     * 复制原始MappedStatement
     *
     * @param oldStatement
     * @param newSqlSource
     * @return
     */
    private MappedStatement copyFromMappedStatement(MappedStatement oldStatement, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(oldStatement.getConfiguration(), oldStatement.getId(), newSqlSource,
                oldStatement.getSqlCommandType());
        builder.resource(oldStatement.getResource());
        builder.fetchSize(oldStatement.getFetchSize());
        builder.statementType(oldStatement.getStatementType());
        builder.keyGenerator(oldStatement.getKeyGenerator());
        if (oldStatement.getKeyProperties() != null) {
            for (String keyProperty : oldStatement.getKeyProperties()) {
                builder.keyProperty(keyProperty);
            }
        }
        builder.timeout(oldStatement.getTimeout());
        builder.parameterMap(oldStatement.getParameterMap());
        builder.resultMaps(oldStatement.getResultMaps());
        builder.cache(oldStatement.getCache());
        builder.useCache(oldStatement.isUseCache());
        return builder.build();
    }

    public static class ZmnSqlSource implements SqlSource {
        BoundSql boundSql;

        public ZmnSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }

    }

    public ZmnDPermitInterceptor(ZmnDPermitSqlParsing zmnPermitDataSqlParsing) {
        this.zmnPermitDataSqlParsing = zmnPermitDataSqlParsing;
    }

    private boolean isSelect(MetaObject metaStatementHandler) {
        return "SELECT".equals(metaStatementHandler.getValue("delegate.mappedStatement.sqlCommandType").toString());
    }

    @SuppressWarnings("unchecked")
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }
}
